package jannex.mapping;

import jannex.Builder;
import jannex.JannexException;

import java.lang.reflect.Constructor;
import java.lang.reflect.Field;
import java.lang.reflect.Method;

public class BuilderFactory {

    public BuilderFactory() {
    }

    public <E> Builder<E> create(final Class<E> cls) {

        final Builder<E> constructorBuilder = constructorBuilder(cls);
        final Builder<E> fallbackBuilder;

        if (cls.isLocalClass() || cls.isAnonymousClass()) {
            fallbackBuilder = nonStaticInnerBuilder(cls);
        } else {
            fallbackBuilder = unsafeBuilder(cls);
        }

        return new Builder<E>() {
            @Override
            public E build() {
                try {
                    if (constructorBuilder != null) {
                        return constructorBuilder.build();
                    }
                } catch (Exception ignored) {
                }
                try {
                    if (fallbackBuilder != null) {
                        return fallbackBuilder.build();
                    }
                } catch (Exception ignored) {
                }

                throw new JannexException("Failed to instantiate object of class " + cls.getName() +
                        ", add a no-args constructor or register an object queryBuilder to solve this.");
            }
        };
    }

    private <E> Builder<E> nonStaticInnerBuilder(final Class<E> cls) {
        final Class<?> enclosingClass = cls.getEnclosingClass();
        final Builder<?> enclosingBuilder = create(enclosingClass);
        return new Builder<E>() {
            @Override
            public E build() {
                try {
                    Object enclosing = enclosingBuilder.build();
                    Constructor<E> declaredConstructor = cls.getDeclaredConstructor(enclosingClass);
                    if (!declaredConstructor.isAccessible()) {
                        declaredConstructor.setAccessible(true);
                    }
                    return declaredConstructor.newInstance(enclosing);
                } catch (Exception ignored) {
                    throw new JannexException("Failed to instantiate anonymous class " + cls.getName() + ".");
                }
            }
        };
    }

    private <E> Builder<E> constructorBuilder(final Class<E> cls) {
        try {
            final Constructor<E> constructor = cls.getDeclaredConstructor();
            if (!constructor.isAccessible()) {
                constructor.setAccessible(true);
            }

            return new Builder<E>() {
                @Override
                public E build() {
                    try {
                        return constructor.newInstance();
                    } catch (Exception e) {
                        throw new RuntimeException(e);
                    }
                }
            };
        } catch (Exception ignored) {
            return null;
        }
    }

    @SuppressWarnings("unchecked")
    private <E> Builder<E> unsafeBuilder(final Class<E> cls) {
        try {
            final Class<?> unsafeClass = Class.forName("sun.misc.Unsafe");
            Field field = unsafeClass.getDeclaredField("theUnsafe");
            field.setAccessible(true);
            final Method allocateInstance = unsafeClass.getMethod("allocateInstance", Class.class);
            return new Builder<E>() {
                @Override
                public E build() {
                    try {
                        return (E) allocateInstance.invoke(unsafeClass, cls);
                    } catch (Exception e) {
                        throw new RuntimeException(e);
                    }
                }
            };
        } catch (Exception ignored) {
            return null;
        }
    }

}
